#! python
# -*- coding: utf-8 -*-

import urllib
import urllib2
import re
import json
import mimetypes
import pprint
import datetime
import time
import yarsa


class YandexError(Exception):
    pass


class YandexService(object):
    def __init__(self, username, password='', token=''):
        """
        Инициализировать работу с сервисом Яндекс.Фотки
        @param username: имя пользователя
        @param password: пароль
        @param token: авторизационный токен, может быть указан вместо пароля
        """
        self.username = username
        self.password = password
        self.token = token

        self.data = {}
        self.albums_href = ''
        self.photos_href = ''
        self.tags_href = ''

    def auth(self, strict=True):
        """
        Запрос авторизационного токена. Если все прошло удачно,
        то внутри сервиса будет установлен авторизационный токен, иначе
        будет вызвано исключение HTTPError
        """

        def extract(tag, text):
            match = re.search('<{0}>(.*?)</{0}>'.format(tag), text)
            return match.group(1) if match else ''

        # Шаг 1. Получение открытого ключа
        answer = urllib2.urlopen('http://auth.mobile.yandex.ru/yamrsa/key/')
        answer = answer.read()
        # Извлечение данных об открытом ключе
        public_key = extract('key', answer)
        request_id = extract('request_id', answer)
        # Шаг 2. Шифрование данных открытым ключом
        message = '<credentials login="{0}" password="{1}"/>'
        message = message.format(self.username, self.password)
        encoded = yarsa.encode(public_key, message)
        # Шаг 3. Передеча данных на сервер яндекса
        request = urllib2.Request('http://auth.mobile.yandex.ru/yamrsa/token/')
        data = urllib.urlencode({'request_id': request_id, 'credentials': encoded})
        request.add_data(data)
        request.add_header('Content-Length', len(data))
        request.add_header('Content-Type', 'application/x-www-form-urlencoded')
        answer = urllib2.urlopen(request)
        answer = answer.read()
        # Извлекаем токен из ответа сервера
        self.token = extract('token', answer)
        if strict and not self.token:
            raise YandexError('Token is empty')

    def request(self, url, data=None, headers=None, code=200, method=None):
        """
        Запрос к сервису Яндекс.Фотки
        При ошибках возникнет исключение HTTPError
        @param url:     адрес запроса
        @param data:    данные, которые будут переданы в теле запроса
        @param headers: дополнительные заголовки
        @param code:    ожидаемый код завершения запроса
        @param method:  если требуется, можно указать нестандартный метод,
                        к примеру PUT
        @return:        результат запроса - документ JSON, который парсится и возвращается в
                        виде стандартных объектов Python.
        """
        request = urllib2.Request(url)
        request.add_header('Accept', 'application/json')
        # проверяем авторизацию
        if self.token:
            request.add_header('Authorization',
                               'FimpToken realm="fotki.yandex.ru", token="{0}"'.format(self.token))
        # Если есть данные, добавляем данные
        if data:
            request.add_data(data)
            request.add_header('Content-Length', len(data))
        # Если есть дополнительные заголовки, то добавляем их
        if headers:
            for h in headers:
                request.add_header(h, headers[h])
        # грязный трюк
        if method:
            request.get_method = lambda: method
        # добавляем дополнительные параметры запроса
        # empty
        # Выполняем запрос
        answer = urllib2.urlopen(request)
        # Проверяем полученный код ответа
        if answer.code != code:
            # N.B. хрен знает, что значит последний параметр fp. Поставил None
            raise urllib2.HTTPError(url, answer.code, 'Bad code', {}, None)
        answer = answer.read()
        return json.loads(answer) if answer else {}

    def load(self, username='', password='', token='', strict=True):
        """
        Загрузить мета-информацию о профиле пользователя на сервисе.
        Если указать имя пользователя, то будет загружена информация о конкретном пользователе.
        @param username: имя пользователя
        @param password: пароль
        @param token: авторизационный токен, может быть указан вместо пароля
        """
        if username:
            self.username = username
            self.password = password
            self.token = token
        if not self.token and self.password:
            self.auth(strict)
        url = 'http://api-fotki.yandex.ru/api/users/{0}/'.format(self.username)
        self.data = self.request(url)
        self.albums_href = self.data['collections']['album-list']['href']
        self.photos_href = self.data['collections']['photo-list']['href']
        self.tags_href = self.data['collections']['tag-list']['href']

    def is_auth(self):
        return bool(self.token)

    def iter_entries(self, url, scheme, count=None, rlimit=100):
        """
        Возвращает итератор по объектам, которые определяются параметром scheme.

        @param url:    адрес коллекции
        @param scheme: Объект, в который будут преобразованы элементы
        @type  count:  int
        @param count:  количество возвращаемых объектов,
                       None - все объекты
        @type  rlimit: int
        @param rlimit: количество объектов, возвращаемых в одном запросе,
                       но не больше 100 (ограничение Яндекс для постраничной
                       выдачи коллекций)
        @rtype:        __generator
        """
        # проверяем, чтобы максимально в выдаче стояло не больше 100 элементов
        if rlimit > 100:
            rlimit = 100
        # если количество запрашиваемых элементов не больше 100, столько и запросим
        if count and count < 100:
            rlimit = count
        # добавляем параметр лимита в запрос
        next_item = url + ('?' if url.find('?') < 0 else '&') + 'limit={0}'.format(rlimit)
        while next_item:
            data = self.request(next_item)
            for i in data['entries']:
                yield scheme(self, i)
                if count is not None:
                    count -= 1
                    if count <= 0:
                        return
            next_item = data['links'].get('next')

    def iter_albums(self, count=None, rlimit=100):
        """
        Возвращает итератор по альбомам. Если вы хотите построить иерархию альбомов,
        разумнее воспользоваться методом get_albums для получения сразу всех альбомов.
        @type  count: int
        @param count: количество возвращаемых альбомов,
                      None - все альбомы
        @type  rlimit: int
        @param rlimit: количество альбомов, возвращаемых в одном запросе,
                       но не больше 100 (ограничение Яндекс для постраничной
                       выдачи коллекций)
        @rtype: __generator of YandexAlbum
        """
        return self.iter_entries(self.albums_href, YandexAlbum, count, rlimit)

    @property
    def albums(self):
        """
        Возвращает все альбомы. Если альбомов очень много, разумнее
        воспользоваться генератором iter_albums.
        @rtype: list of YandexAlbum
        """
        return list(self.iter_albums())

    def iter_photos(self, count=None, rlimit=100):
        """
        @type  count: int
        @param count: количество возвращаемых фотографий,
                      None - все фотографии
        @type  rlimit: int
        @param rlimit: количество фотографий, возвращаемых в одном запросе,
                       но не больше 100 (ограничение Яндекс для постраничной
                       выдачи коллекций)
        @rtype: __generator of YandexPhoto
        """
        return self.iter_entries(self.photos_href, YandexPhoto, count, rlimit)

    @property
    def photos(self):
        """
        Возвращает все фотографии. Если фотографий очень много, разумнее
        воспользоваться генератором iter_photos.
        @rtype: list of YandexPhoto
        """
        return list(self.iter_photos())

    def iter_tags(self, count=None, rlimit=100):
        return self.iter_entries(self.tags_href, YandexTag, count, rlimit)

    def create_album(self, title, summary=None, parent=None):
        """
        Создать новый альбом
        @param title: Название
        @param summary: Описание
        @type parent: YandexAlbum
        @param parent: Родительский альбом
        @return: Новый созданный альбом
        """
        data = {'title': title}
        if summary:
            data['summary'] = summary
        if parent:
            data['links'] = {'album': parent.link_self}
        headers = {'Content-Type': 'application/json;  type=entry'}
        answer = self.request(self.albums_href, json.dumps(data), headers, code=201)
        return YandexAlbum(self, answer)

    def __str__(self):
        return pprint.pformat(self.data)

    @property
    def id(self):
        """Уникальный идентификатор сущности"""
        return None

    @property
    def link_self(self):
        return None


class YandexEntry(object):
    def __init__(self, service, data):
        """
        @type   service: YandexService
        @type   data: dict
        @param  data: словарь параметров сущности
        """
        self.service = service
        self.data = data

    @staticmethod
    def convert_time(ts, tz=True):
        if not ts:
            return None
        dt = datetime.datetime.strptime(ts, '%Y-%m-%dT%H:%M:%SZ')
        if tz:
            dt = dt - datetime.timedelta(seconds=time.timezone)
        return dt

    @property
    def id(self):
        """Уникальный идентификатор сущности"""
        return self.data['id']

    @property
    def title(self):
        """Заголовок сущности"""
        return self.data.get('title', '')

    def link(self, name='self'):
        return self.data['links'].get(name)

    @property
    def link_self(self):
        return self.link('self')

    @property
    def link_edit(self):
        return self.link('edit')

    @property
    def link_parent(self):
        return self.link('album')

    @property
    def link_alt(self):
        return self.link('alternate')

    @property
    def created(self):
        return self.convert_time(self.data.get('created'), False)

    @property
    def edited(self):
        return self.convert_time(self.data.get('edited'))

    @property
    def published(self):
        return self.convert_time(self.data.get('published'))

    def update(self):
        self.data = self.service.request(self.link_self)

    def edit(self, upd_data):
        data = self.service.request(self.link_edit)
        data.update(upd_data)
        data = json.dumps(data)
        headers = {'Content-Type': 'application/json;  type=entry'}
        self.data = self.service.request(self.link_self, data, headers, method='PUT')

    def delete(self):
        self.service.request(self.link_self, code=204, method='DELETE')

    # def __str__(self):
    # return pprint.pformat(self.data)

    def __eq__(self, other):
        return self.id == other.id


class YandexAlbum(YandexEntry):
    @property
    def link_photos(self):
        return self.link('photos')

    @property
    def photos(self):
        return list(self.iter_photos())

    @property
    def count(self):
        return self.data.get('imageCount', 0)

    def upload(self, filename):
        # При загрузке данных надо обернуть все в bytearray, иначе происходит ошибка Unicode
        # чертовы строки в python
        # http://bugs.python.org/issue12398
        data = bytearray(open(filename, 'rb').read())
        mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
        headers = {'Content-Type': mimetype}
        answer = self.service.request(self.link_photos, data, headers, code=201)
        photo = YandexPhoto(self.service, answer)
        return photo

    def edit(self, title=None, summary=None, parent=None):
        data = {}
        if title is not None:
            data['title'] = title
        if summary is not None:
            data['summary'] = summary
        if parent is not None:
            data['links'] = {'album': parent.get_link()}
        YandexEntry.edit(self, data)

    def iter_photos(self, count=None, rlimit=100):
        return self.service.iter_entries(self.link_photos, YandexPhoto, count, rlimit)

    def create_album(self, title, summary=None):
        return self.service.create_album(title, summary, self)


class YandexPhoto(YandexEntry):
    def __init__(self, service, data):
        YandexEntry.__init__(self, service, data)
        imgs = self.data['img']
        self.images = sorted([YandexImage(self, name, imgs[name]) for name in imgs],
                             key=lambda x: x.w)

    def image(self, name):
        return next((x for x in self.images if x.name == name), None)

    def edit(self, title=None, summary=None, xxx=None,
             disable_comments=None, hide_original=None, access=None):
        data = {}
        if title is not None:
            data['title'] = title
        if summary is not None:
            data['summary'] = summary
        if xxx is not None:
            data['xxx'] = bool(xxx)
        if disable_comments is not None:
            data['disableComments'] = bool(disable_comments)
        if hide_original is not None:
            data['hideOriginal'] = bool(hide_original)
        if access is not None:
            data['access'] = access
        YandexEntry.edit(self, data)

    @property
    def tags(self):
        return self.data.get('tags', {})


class YandexImage(object):
    def __init__(self, photo, name, data):
        self.photo = photo
        self.name = name
        self.w = data['width']
        self.h = data['height']
        self.size = data.get('bytesize')
        self.href = data['href']

    def download(self):
        return urllib2.urlopen(self.href).read()

    def __repr__(self):
        return '<{} {}x{}>'.format(self.name, self.w, self.h)


class YandexTag(object):
    def __init__(self, data):
        self.data = data

    def __str__(self):
        return pprint.pformat(self.data)


if __name__ == '__main__':
    try:
        s = YandexService(None)
        s.load()
        print 'Token', s.token
        ph = next(s.iter_photos())
        pprint.pprint(ph.data)
        # for t in s.iter_tags():
        # print t

    except urllib2.HTTPError as ex:
        print ex
